package cn.z2huo.usercenter.authentication.app.aspect;

import cn.z2huo.usercenter.authentication.model.vo.login.test.UserLoginReqVO;
import cn.z2huo.usercenter.util.RedisKeyGenerateUtils;
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.redisson.api.RList;
import org.redisson.api.RScoredSortedSet;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;

/**
 * <p>滑动窗口限流
 *
 * @author z2huo
 */
@Aspect
@Component
@RequiredArgsConstructor
//@Order(1)
public class SlidingWindowLoginLimitAspect {

    private final RedissonClient redissonClient;

    private static final int LIMIT_COUNT = 5;

    private static final Duration LIMIT_TIME_DURATION = Duration.ofSeconds(10);

    @Pointcut("execution(* cn.z2huo.usercenter.authentication.app.controller.login.test.LoginLimitController.login(..)) && args(reqVO)")
    public void pointcut(UserLoginReqVO reqVO) {
    }

    @Before(value = "pointcut(reqVO)", argNames = "reqVO")
    public void before(UserLoginReqVO reqVO) {

//        boolean flag = list(reqVO.getUserCode(), LIMIT_COUNT, LIMIT_TIME_DURATION);
        boolean flag = zset(reqVO.getUserCode(), LIMIT_COUNT, LIMIT_TIME_DURATION);
        if (!flag) {
            throw new RuntimeException("限制时间内登陆次数过多，请稍后再试");
        }

    }

    private boolean list(String userCode, int limitCount, Duration limitTimeDuration) {

        String redisKey = RedisKeyGenerateUtils.AUTHENTICATION_APP.generateMd5("user-login-list-sliding-window", userCode);

        RList<Long> list = redissonClient.getList(redisKey);

        long now = LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli();

        if (list.isExists()) {
            int size = list.size();
            // 如果队列还没有达到上线，则允许通过，并添加当前时间到队列开始为止
            if (size < limitCount) {
                list.addFirst(now);
                return true;
            }
            // 如果队列已满，则获取最后一个时间戳（最早添加的）
            Long lastElement = list.getLast();
            // 如果当前时间和最早元素时间戳间隔在限制时间段内
            // 表示这一时间段内已经通过次数达到限制，则不允许通过
            if (now - lastElement <= limitTimeDuration.toMillis()) {
                return false;
            } else {
                // 如果间隔不在时间段内
                // 表示在该时间段内通过次数小于限制，则允许通过
                // 并删除最早的时间戳，江当前时间戳加到队列开始位置
                list.removeLast();
                list.addFirst(now);
                return true;
            }
        } else {
            list.addFirst(now);
        }
        return true;
    }

    /**
     * 基于 zset 实现的滑动窗口
     *
     * 如果元素过多，zset 中存放数据过多，考虑集群倾斜 和 big key
     * @param userCode
     * @param limitCount
     * @param limitTimeDuration
     * @return
     */
    private boolean zset(String userCode, int limitCount, Duration limitTimeDuration) {

        String redisKey = RedisKeyGenerateUtils.AUTHENTICATION_APP.generateMd5("user-login-zset-sliding-window", userCode);

        RScoredSortedSet<Object> set = redissonClient.getScoredSortedSet(redisKey);

        long now = LocalDateTime.now().atZone(ZoneId.of("Asia/Shanghai")).toInstant().toEpochMilli();

        // 移除过期的元素，只保留最近一分钟的数据
        // 分数为时间戳，递增，所以最近的时间戳比较大，在 set 中的分数也就比较大
        // 移除元素时，从 0 开始移除，剩下的就是最近一分钟的元素
        set.removeRangeByScore(0, true, now - limitTimeDuration.toMillis(), true);
        // 将当前时间加入到排序集合中
        set.add(now, now);
        // 集合中的元素大于时间窗口中限制的数量
        return set.size() <= limitCount;
    }

}
